package parts.mass;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;

import java.util.List;
import applet.Simulation;
import applet.forces.Force;
import applet.forces.GlobalForce;
import applet.forces.WallData;


/**
 * Mass holds the information for a mass object.
 * It also tells the simulation how to update each
 * mass object depending on the forces acting on it.
 * For the global forces, Mass will look at the info
 * in GlobalForce class and update each mass according
 * to the currently active global forces.
 *
 * @author Robert C. Duvall
 * edited by Jesse Starr and Guy Tracy
 */
public class Mass {
    // state
    private Point2D myCenter;
    private Force myVelocity;
    private Dimension mySize;
    private double myID;
    private double myMass;
    private Force myAcceleration;

    /**
     * @param id of the mass (should be > 0 if from a file)
     * @param x location of the mass
     * @param y location of the mass
     * @param mass of the mass
     */
    public Mass (double id, double x, double y, double mass) {
        myAcceleration = new Force();
        myMass = mass;
        myID = id;
        setCenter(x, y);
        setVelocity(0, 0);
        mySize = new Dimension(16, 16);
    }

    /**
     * Paints the mass on the screen.
     *
     * @param pen used to paint
     */
    public void paint (Graphics2D pen) {
        pen.setColor(Color.BLACK);
        pen.fillOval(getLeft(), getTop(), getSize().width, getSize().height);
    }

    /**
     * Updates the masses on the screen after checking for any
     * global forces.
     *
     * @param canvas on which the masses will be updated
     * @param dt is the time change
     * @param globals is all the global forces in effect
     */
    public void update (Simulation canvas, double dt, GlobalForce globals) {

        if (globals.gravityCheck()) {
            applyForce(getGravity(globals.getGDirection(),
                    globals.getGMagnitude()));
        }

        if (globals.viscosityCheck()) {
            applyForce(getViscosity(globals.getVMagnitude()));
        }

        if (globals.centerCheck()) {
            applyForce(getCenterOfMass(globals.getCOMPoint(),
                    globals.getCOMMagnitude(), globals.getCOMExponent()));
        }

        applyForce(getWallRepulsion(canvas, globals));
        applyForce(getBounce(canvas));
        // convert force back into Mover's velocity
        getVelocity().sum(myAcceleration);
        myAcceleration.reset();
        // move mass by velocity
        myCenter.setLocation(myCenter.getX() + myVelocity.getXChange() * dt,
                myCenter.getY() + myVelocity.getYChange() * dt);
    }

    /**
     * Repelling force off of a wall only if that
     * wall's force is currently active.
     *
     * @param canvas gives the bounds
     * @param globals gives which wall has the force
     * @return
     */
    private Force getWallRepulsion (Simulation canvas, GlobalForce globals) {

        List<WallData> myWallData = globals.getWallData();
        Force totalWallForce = new Force();
        for (WallData w : myWallData) {
            if (w.checkWall()) {
                totalWallForce.sum(w.getWallForce(canvas, myCenter));
            }
        }

        return totalWallForce;
    }

    /**
     * Returns the viscosity force for this mass.
     *
     * @param magnitude of the force
     * @return
     */
    private Force getViscosity (double magnitude) {
        Force dragForce = new Force(getVelocity().getAngle(),
                getVelocity().getMagnitude());

        dragForce.negate();
        dragForce.scale(magnitude);
        return dragForce;
    }

    /**
     * Returns the center of mass force on this mass.
     *
     * @param centerOfMass is the point to which this
     * mass is attracted.
     * @param magnitude of the force
     * @param exp for scaling the force to distance
     * @return
     */
    private Force getCenterOfMass (Point2D centerOfMass, double magnitude,
            double exp) {
        double dx = myCenter.getX() - centerOfMass.getX();
        double dy = myCenter.getY() - centerOfMass.getY();
        double distance = Math.sqrt(dx * dx + dy * dy);
        double angle = angleBetween(centerOfMass, myCenter);
        double tempMag = 1 / Math.pow(distance, exp);

        Force result = new Force(angle, tempMag);
        result.scale(magnitude);

        return result;
    }

    /**
     * @param f
     *        force to be applied to this mass
     */
    public void applyForce (Force f) {
        myAcceleration.sum(f);
    }

    /**
     * Check if mass is 'id' mass.
     * @param id of the mass
     */
    public boolean match (double id) {
        return myID == id;
    }

    /**
     * Returns the gravitational force.
     *
     * @param direction
     * @param magnitude
     * @return
     */
    private Force getGravity (double direction, double magnitude) {
        Force result = new Force(direction, magnitude);

        result.scale(1.0 / myMass);
        return result;
    }

    /**
     * Returns a force to bounce the mass off walls.
     *
     * @param canvas
     * @return
     */
    private Force getBounce (Simulation canvas) {
        Force impulse = new Force();

        if (getLeft() < canvas.getLeft()) {
            impulse = new Force(0, 2);
            setCenter(getSize().width / 2, getCenter().getY());
        }
        else if (getRight() > canvas.getRight()) {
            impulse = new Force(180, 2);
            setCenter(canvas.getRight() - getSize().width / 2,
                    getCenter().getY());
        }
        if (getTop() < canvas.getTop()) {
            impulse = new Force(270, 2);
            setCenter(getCenter().getX(), getSize().height / 2);
        }
        else if (getBottom() > canvas.getBottom()) {
            impulse = new Force(90, 2);
            setCenter(getCenter().getX(),
                    canvas.getBottom() - getSize().height / 2);

        }
        impulse.scale(getVelocity().getRelativeMagnitude(impulse));
        return impulse;
    }

    /**
     * Returns shape's velocity.
     */
    public Force getVelocity () {
        return myVelocity;
    }

    /**
     * Returns this mass's id.
     * @return
     */
    public double getID () {
        return myID;
    }

    /**
     * Resets shape's velocity.
     *
     * @param direction of velocity
     * @param magnitude of velocity
     */
    public void setVelocity (double direction, double magnitude) {
        myVelocity = new Force(direction, magnitude);
    }

    /**
     * Returns shape's center.
     */
    public Point2D getCenter () {
        return myCenter;
    }

    /**
     * Resets shape's center.
     *
     * @param x of new center
     * @param y of new center
     */
    public void setCenter (double x, double y) {
        myCenter = new Point2D.Double(x, y);
    }

    /**
     * Returns shape's left-most coordinate.
     */
    public int getLeft () {
        return (int) (getCenter().getX() - getSize().width / 2);
    }

    /**
     * Returns shape's top-most coordinate.
     */
    public int getTop () {
        return (int) (getCenter().getY() - getSize().height / 2);
    }

    /**
     * Returns shape's right-most coordinate.
     */
    public int getRight () {
        return (int) (getCenter().getX() + getSize().width / 2);
    }

    /**
     * Reports shape's bottom-most coordinate.
     *
     * @return bottom-most coordinate
     */
    public int getBottom () {
        return (int) (getCenter().getY() + getSize().height / 2);
    }

    /**
     * Returns shape's size.
     */
    public Dimension getSize () {
        return mySize;
    }

    /**
     * Returns shape's acceleration.
     */
    public Force getForce () {
        return myAcceleration;
    }

    /**
     * Returns shape's mass.
     */
    public double getMass () {
        return myMass;
    }

    /**
     * Gets the distance from this mass to a point.
     *
     * @param point to get distance from
     * @return
     */
    public double getDistFromMass(Point2D point) {
        double dx = point.getX() - this.getCenter().getX();
        double dy = point.getY() - this.getCenter().getY();
        double distance = Math.sqrt(dx * dx + dy * dy);
        return distance;
    }

    /**
     * Returns the angle between the given two points.
     */
    private double angleBetween (Point2D p1, Point2D p2) {
        return angleBetween(p1.getX() - p2.getX(), p1.getY() - p2.getY());
    }

    /**
     * Returns the angle represented by the given dx and dy.
     */
    private double angleBetween (double dx, double dy) {
        return Math.toDegrees(Math.atan2(dy, dx));
    }
}
